package ui;

import static org.lwjgl.opengl.GL11.GL_AMBIENT;
import static org.lwjgl.opengl.GL11.GL_AMBIENT_AND_DIFFUSE;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_COLOR_MATERIAL;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_DIFFUSE;
import static org.lwjgl.opengl.GL11.GL_EMISSION;
import static org.lwjgl.opengl.GL11.GL_FILL;
import static org.lwjgl.opengl.GL11.GL_FRONT;
import static org.lwjgl.opengl.GL11.GL_FRONT_AND_BACK;
import static org.lwjgl.opengl.GL11.GL_LIGHT0;
import static org.lwjgl.opengl.GL11.GL_LIGHTING;
import static org.lwjgl.opengl.GL11.GL_LIGHT_MODEL_AMBIENT;
import static org.lwjgl.opengl.GL11.GL_LINE_STRIP;
import static org.lwjgl.opengl.GL11.GL_NORMALIZE;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_POLYGON_MODE;
import static org.lwjgl.opengl.GL11.GL_POSITION;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_SHININESS;
import static org.lwjgl.opengl.GL11.GL_SMOOTH;
import static org.lwjgl.opengl.GL11.GL_SPECULAR;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glClearColor;
import static org.lwjgl.opengl.GL11.glColorMaterial;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glFlush;
import static org.lwjgl.opengl.GL11.glGetInteger;
import static org.lwjgl.opengl.GL11.glLight;
import static org.lwjgl.opengl.GL11.glLightModel;
import static org.lwjgl.opengl.GL11.glMaterial;
import static org.lwjgl.opengl.GL11.glMaterialf;
import static org.lwjgl.opengl.GL11.glPolygonMode;
import static org.lwjgl.opengl.GL11.glReadBuffer;
import static org.lwjgl.opengl.GL11.glReadPixels;
import static org.lwjgl.opengl.GL11.glShadeModel;
import static org.lwjgl.opengl.GL11.glVertex2f;

import java.awt.event.KeyEvent;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.text.DecimalFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.AWTGLCanvas;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;

import tools.BufferTools;
import tools.PointCloudViewer;
import triangulation.SimpleTriangulation;
import util.Camera;
import util.LightingOptions;
import util.ScreenshotWriter;

public class ViewerCanvas extends AWTGLCanvas
{
	private static final float CAMERA_TRANSLATE_X = 2.0f;
	private static final float CAMERA_TRANSLATE_Y = 2.0f;
	private static final float CAMERA_TRANSLATE_Z = 2.0f;
	private static final float CAMERA_ROTATE_X = 2.0f;
	private static final float CAMERA_ROTATE_Y = 2.0f;
	private static final float CAMERA_ROTATE_Z = 2.0f;
	private static final float CAMERA_SPEED_UP = 6.0f; // multiply this with translate or rotate when shift is held down 
	
	private static UnicodeFont font;
	private static DecimalFormat formatter;
	
	public Camera camera = null;
	public PointCloudViewer viewer = null;
	public ByteBuffer framebuffer = null; // only set this when the user wants to take a screenshot
	
	int current_height;
	int current_width;
	
	private boolean bLightsOn = false;
	private LightingOptions lights;
	
	private ExecutorService service;
	private boolean _runThread_Screenshot = false;
	private File _screenshotFile = null;
	
	private ColorScale wavelengthScale = null;
	private ColorScaleHue hueScale = null;
	private ColorScaleGray grayScale = null;
	private ColorSquare squareScale = null;
	
	public double dClusterEpsilon = 1.0;
	public double dClusterDistance = 1.0;
	

	public ViewerCanvas() throws LWJGLException
	{
		super();

		viewer = new PointCloudViewer();
		camera = new Camera();
		
		wavelengthScale = new ColorScale();
		hueScale = new ColorScaleHue();
		grayScale = new ColorScaleGray();
		squareScale = new ColorSquare();
		
		service = Executors.newSingleThreadExecutor(); // allows us to queue up background threads
	}
	
	public void enableTextRendering()
	{
	    glEnable(GL_BLEND);
	    glEnable(GL_TEXTURE_2D);
	}
	
	public void disableTextRendering()
	{
	    glDisable(GL_BLEND);
	    glDisable(GL_TEXTURE_2D);
	}
	
	public void enableLights()
	{
		glEnable(GL_LIGHTING);
		glEnable(GL_LIGHT0);
	}
	
	public void disableLights()
	{
		glDisable(GL_LIGHTING);
		glDisable(GL_LIGHT0);
	}
	
	public void toggleLights()
	{
		if (bLightsOn) disableLights();
		else enableLights();
		bLightsOn = !bLightsOn;
	}
		
	private void initLighting()
	{
	    glShadeModel(GL_SMOOTH);
	    glEnable(GL_NORMALIZE);
	    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	    glEnable(GL_DEPTH_TEST);
	    
        glLightModel(GL_LIGHT_MODEL_AMBIENT, BufferTools.asFlippedFloatBuffer(new float[] {1.0f, 1.0f, 1.0f, 1.0f}));
        
		lights = new LightingOptions();
		glLight(GL_LIGHT0, GL_AMBIENT, lights.ambient);
		glLight(GL_LIGHT0, GL_DIFFUSE, lights.diffuse);
		glLight(GL_LIGHT0, GL_SPECULAR, lights.specular);
		glLight(GL_LIGHT0, GL_POSITION, lights.position);
		
		glMaterial(GL_FRONT, GL_AMBIENT, lights.ambient);
		glMaterial(GL_FRONT, GL_DIFFUSE, lights.diffuse);
		glMaterial(GL_FRONT, GL_SPECULAR, lights.specular);
		glMaterial(GL_FRONT, GL_EMISSION, lights.emission);
		glMaterialf(GL_FRONT, GL_SHININESS, lights.shininess.get(0));

		glEnable(GL_COLOR_MATERIAL);
		glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
	}

	// setup the fonts using slick.jar
    private static void setUpFonts()
    {
    	System.out.print("Loading fonts... ");
        java.awt.Font awtFont = new java.awt.Font("Ariel", java.awt.Font.PLAIN, 16);
        font = new UnicodeFont(awtFont);
        font.addAsciiGlyphs();
        font.getEffects().add(new ColorEffect(java.awt.Color.white));
        
        formatter = new DecimalFormat("#.###"); 
        try
        {
            font.loadGlyphs();
        } catch (SlickException e)
        {
            e.printStackTrace();
        }
    	
        System.out.println(" done!");
    }
	
	protected void initGL()
	{
		camera.init();

	    glClearColor(0.f, 0.f, 0.f, 0.f);
	    glEnable(GL_DEPTH_TEST); // Enables Depth Testing
	    
		initLighting();
		setUpFonts();
		
		// using display lists for this scale so we need to generate them after we've init'd OpenGL
		// when we start using display lists for the other scales we will need to move them here
		squareScale.regenerate(); 
	}
	
	public void paintGL()
	{
		try
		{
//			if (getWidth() != current_width || getHeight() != current_height)
//			{
//				current_width = getWidth();
//				current_height = getHeight();
//				glViewport(0, 0, current_width, current_height);
//			}
			glClear(GL11.GL_COLOR_BUFFER_BIT|GL11.GL_DEPTH_BUFFER_BIT);

			
			if (_runThread_Screenshot)
			{
				service.submit(new ScreenshotWriter(_screenshotFile, captureScreen()));
				_runThread_Screenshot = false;
				_screenshotFile = null;
			}
			
			camera.update();
			
			if (viewer.isLoaded())
			{
//				if (triangulation.isLightsEnabled()) enableLights();
//				else disableLights();
				
				enableLights();

				viewer.render();
				boolean bCreated = viewer.createGeometry(); // true if first time triangulating the point cloud
				if (bCreated)
				{
					float x = (float)(viewer.xBoundMin + viewer.getBoundX_Legnth()/2);
					float y = (float)(viewer.yBoundMin + viewer.getBoundY_Legnth()*0.75);
					float z = (float)viewer.getBoundZ_Legnth();
					
					// set the defaults to the centers
					UIConst.DEFAULT_POS_X = x;
					UIConst.DEFAULT_POS_Y = y;
					UIConst.DEFAULT_POS_Z = z;
					camera.set(x, y, z);
				}
			}
			
			drawCameraLocation();

			glFlush();
			swapBuffers();
			repaint();
		}
		catch (LWJGLException e)
		{
			throw new RuntimeException(e);
		}
	}
	
	private void drawColorSquare()
	{
		int prevMode = glGetInteger(GL_POLYGON_MODE);  
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		
		glDisable(GL_LIGHTING); // do not want lighting to affect the 2D ortho projections		
		
		int offset_x = ColorSquare.pxSlideLength;
		int offset_y = ColorSquare.pxSlideLength + UIConst.OPTIONS_PANEL_OFFSET;
		
		squareScale.render();

		
		Camera.setOrthoON();
		
		enableTextRendering();
		
		// draw the range as strings, get the position of the
		font.drawString(10.0f + offset_x, 25.0f + offset_y, formatter.format(360.0) + "/" + formatter.format(90.0));
		font.drawString(10.0f, UIConst.OPTIONS_PANEL_OFFSET, formatter.format(0.0));

		disableTextRendering();
		
		Camera.setOrthoOFF();
		
		glEnable(GL_LIGHTING);
		
		glPolygonMode(GL_FRONT_AND_BACK, prevMode);
	}

	private void drawCameraLocation()
	{
		int prevMode = glGetInteger(GL_POLYGON_MODE);  
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

		enableTextRendering();
		
		Camera.setOrthoON();
		glDisable(GL_LIGHTING);

		float[] pos = camera.getPosition();
		String strCam = "Camera: [x=" + formatter.format(pos[0]) + ",y=" + formatter.format(pos[1]) + ",z=" + formatter.format(pos[2]) + "]"; 
		int strWidth = font.getWidth(strCam) + 5;
		int strHeight = font.getHeight(strCam) + 5;
        font.drawString(UIConst.APP_WIDTH - strWidth - 10, strHeight + 30, strCam);
        
        glEnable(GL_LIGHTING);
		Camera.setOrthoOFF();
		
		disableTextRendering();
		
		glPolygonMode(GL_FRONT_AND_BACK, prevMode);
	}

	public void move_mesh(KeyEvent e)
	{
		// handle the shift key for sped up translate and rotation		
		float spd = 1.0f;
		boolean bShift = (e.getModifiersEx() & KeyEvent.SHIFT_DOWN_MASK) == KeyEvent.SHIFT_DOWN_MASK;
		if (bShift) spd = CAMERA_SPEED_UP;
		
		// key strokes move the mesh in the same direction as the name implies (eg: VK_LEFT = move to left)
		if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_A) camera.translate(CAMERA_TRANSLATE_X*spd, 0.0f, 0.0f);
		if (e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_D) camera.translate(-CAMERA_TRANSLATE_X*spd, 0.0f, 0.0f);
		if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_W) camera.translate(0.0f, -CAMERA_TRANSLATE_Y*spd, 0.0f);
		if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_S) camera.translate(0.0f, CAMERA_TRANSLATE_Y*spd, 0.0f);
		if (e.getKeyCode() == KeyEvent.VK_Z) camera.translate(0.0f, 0.0f, -CAMERA_TRANSLATE_Z*spd); // zoom out
		if (e.getKeyCode() == KeyEvent.VK_C) camera.translate(0.0f, 0.0f, CAMERA_TRANSLATE_Z*spd); // zoom in
		
		
		// rotate the triangulation and not the camera allows us to rotate about the triangulation axis
		if (e.getKeyCode() == KeyEvent.VK_Q) viewer.rotate(0.0f, CAMERA_ROTATE_Y*spd, 0.0f); 		// y-axis rotation
		if (e.getKeyCode() == KeyEvent.VK_E) viewer.rotate(0.0f, -CAMERA_ROTATE_Y*spd, 0.0f);
		if (e.getKeyCode() == KeyEvent.VK_R) viewer.rotate(CAMERA_ROTATE_X*spd, 0.0f, 0.0f); 		// x-axis rotation		
		if (e.getKeyCode() == KeyEvent.VK_V) viewer.rotate(-CAMERA_ROTATE_X*spd, 0.0f, 0.0f);
		if (e.getKeyCode() == KeyEvent.VK_F) viewer.rotate(0.0f, 0.0f, CAMERA_ROTATE_Z*spd); 		// x-axis rotation		
		if (e.getKeyCode() == KeyEvent.VK_G) viewer.rotate(0.0f, 0.0f, -CAMERA_ROTATE_Z*spd);
		
		if (e.getKeyCode() == KeyEvent.VK_X)
		{
			camera.reset();
			viewer.reset_rotations();
		}
	}
	
	public FloatBuffer GetOGLPos(int mouseX, int ymouseY)
	{
//		FloatBuffer winZ = BufferUtils.createFloatBuffer(1);
		float winX, winY;
		FloatBuffer position = BufferUtils.createFloatBuffer(3);

		winX = (float)mouseX;
		winY = (float)camera.getViewport().get(3) - (float)ymouseY;
//		glReadPixels(mouseX, (int)winY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, winZ);
		GLU.gluUnProject(winX, winY, 1.0f, camera.getModelView(), camera.getProjection(), camera.getViewport(), position);
		return position;
	}
	public void printGLPos(int x, int y)
	{
		FloatBuffer pos = GetOGLPos(x, y);
		System.out.println("GLPos: " + pos.get(0)+","+pos.get(1)+","+pos.get(2));
	}
	
	// get the screens pixel data, useful for taking screenshots
	private ByteBuffer captureScreen()
	{
		glReadBuffer(GL_FRONT);
		int width = UIConst.APP_WIDTH;
		int height = UIConst.APP_HEIGHT;
		int bpp = 4; // assumption here is that the display is 32-bit thus we have a byte per rgba
		ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * bpp);
		glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
		return buffer;
	}
	
	// sets flags to allow saving of an image in the background
	public void takeScreenshot(File f)
	{
		_runThread_Screenshot = true;
		_screenshotFile = f;
	}
}
